大家還記得果王大賽中, 因為水果被沒收而忿忿不平的老樹猴嗎? 詳情請參閱Day 4 - 果王大賽.
老樹猴在水果被沒收的打擊中, 萌生了一個偉大的計劃: 「只要掌握台灣的農產品總量, 我就能趁著月黑風高的晚上去偷那些農產品, 以彌補我被徵收的損失了!哈哈哈!」真是異想天開, 但就因為這樣, 老樹猴的黑名單, 誕生了...
獲得農產品資料的老樹猴找上了我們, 替它做一個搜尋的程式. 只要打出相應的農產品名稱或是年度, 就會顯示出該農產品在台灣的生產總量. 符合搜尋欄位的文字還要特別標記, 範例如此連結: 老樹猴的黑名單
雖然沒有完成並不會受到老樹猴的威脅, 但是看他可憐, 我們還是幫幫他吧!
老樹猴的資料被存在遠端伺服器某個叫crop.json
的檔案中, 我們必須先存取該檔案, 放在陣列裡供我們使用. 此時可以使用fetch()
方法.
fetch()
方法fetch()
方法是Fetch API中的一個方法, 使用fetch()
方法能夠讓我們能夠在非同步索取遠端資源時變得更加直覺且容易.
簡單說一下「非同步」, 一般程式執行都是依照安排好的架構一行接著一行的執行, 這我們稱為「同步」. 這種情況下, 只要其中一行因為某些原因無法執行完, 後面的程式就會一直等待.
廣義來說, 只要不是上面這種依序立即執行的狀況, 都稱作「非同步」. 延後執行, 插隊, 同時執行等都是非同步.
跟遠端伺服器要檔案是需要時間的. 如果大家都要等要檔案的動作結束才能跑, 或是因為一直要不到檔案, 網頁無法動作, 這感覺很差的!
因此有了非同步索取遠端檔案這件事. 向遠端要檔案的同時, 讓其他程式碼繼續執行, 然後確定要到檔案了, 再用回呼函式的方式插隊處理要回來的檔案.
早期是用XMLHttpRequest
來執行, fetch()
方法是ES6後的改良版, 中間還有很多故事, 限於篇幅暫不說明.
用法如下, url
表示要索取的對象, 用fetch(url)
向其索取, 傳回來的會是一個稱作Promise
物件的東西.
Promise
物件簡單來說就像一個兌換券的機制, 先拿到兌換券(Promise
), 就肯定能換到商品(遠端檔案), 就能夠先安排拿到商品之後要做些什麼(用Promise
的then
方法).
then
是Promise
物件的方法, 用來安排得到檔案之後要做的事情, then
方法回傳的也是個Promise
物件, 內含有索要的資料. 在then
方法內可以設定用來處理資料的函式.
下面程式碼向老樹猴的的遠端伺服器索要農產資料, 得到資料後將資料轉換成json
格式, 然後再將得到的json
資料存入crops
陣列中.
const endpoint = 'https://gist.githubusercontent.com/godlike0108/772fc95084aeeeb1b9e6157d7d2c9274/raw/45d105c65b17782537a9fa1baf103efbba1226a4/crop.json';
const crops = [];
const prom = fetch(endpoint)
.then(response => response.json())
.then(data => crops.push(...data));
有了資料後, 我們希望每次在輸入欄位打字都能依序執行下列任務:
suggestions
清單內.架構大概如下:
const searchInput = document.querySelector('.search');
const suggestions = document.querySelector('.suggestions');
function findMatches(wordToMatch, crops) {
// 會回傳一個裝著所有符合資料的陣列
}
function displayMatches() {
// 存著所有符合資料的陣列
const matchArray = findMatches(this.value, crops);
// 組合成HTML格式
const html = matchArray.map(crop => {
return `
<li>
<span class="name">${crop['作物名稱']}, ${crop['年度']}</span>
<span class="population">${crop['產量']}</span>
</li>
`;
}).join('');
// 插入到HTML文件中
if(searchInput.value != '') {
suggestions.innerHTML = html;
} else {
suggestions.innerHTML = `
<li>輸入農作物名稱</li>
<li>或是年度</li>
`;
}
}
// 搜尋欄位內容變動時, 呼叫函式
searchInput.addEventListener('input', displayMatches);
這裡比較特別的地方是我們這次監聽了input
事件, 比起change
事件, input
事件只要輸入欄位的內容一更動, 就會觸發事件. 而change
事件是要更動了欄位內容, 接著欄位失焦後, 才會觸發. 在這裡用input
, 就可以讓顯示資料隨著每次輸入而重新篩選.
至於如何篩選出適合的資料呢? 在這裡用到正規表達式(Regular Expressions)
正規表達式(Regular Expression)用來在字串中比對符合規則的字元組合.
可以直接定義字面值(literal)或是藉由建構式(constructor)所創造, 前者多用於搜尋固定的字元組合, 後者用於字元組合常會被重新指定的時候. 範例如下.
// Through RegExp Literal
var regex = /abc/g;
// Through RegExp Constructor
var regex = new RegExp('abc', 'g')
結構上來說, 正規表達式又可分成字元組合本身(pattern), 與標記(flag)兩個部分.
字元組合放在/ /
內部, 可以有一般字串, 特殊符號兩種, 用來定義一串符合某個規則的字串的集合.
標記加在/ /
的後面, 也就是字元組合的後方, 用來限制搜索字元組合的行為.
聽起來很抽象, 用下面程式碼舉例...
//^ 是特殊字元, 代表開頭
// L 是一般字元, 代表L
// ^L 意思為: 開頭要是L
// \d{9}$ 意思為, 結尾要是九個數字
// g, i 都是flag, 限制搜索行為
// g 表示對整個字串全域搜索
// i 表示不分大小寫
// 符合這種格式的, 就是身分證字號的格式
var peopleID = /^L\d{9}$/gi;
peopleID.test('R123456789'); // True!! 符合身分證字號的格式
知道後, 就可以用Regular Expression來篩選需要的資料了, 定義在findMatches
函式內的程式碼如下:
function findMatches(wordToMatch, crops) {
return crops.filter(crop => {
const regex = new RegExp(wordToMatch, 'gi');
return crop['作物名稱'].match(regex) || crop['年度'].match(regex);
});
}
match()
方法是String
物件的原生方法, 括號內為正規表達式, 符合括號內的正規表達式, 才會被留下來.
寫到這邊, 老樹猴的黑名單已經大致完成了, 但是老樹猴視力不太好, 他希望在篩選出來的資料裡面能夠用背景色強調與打在搜尋列的字串相同的字串, 我們修改一下displayMatches
函式裡面html
變數的內容, 讓正規表達式滿足他老人家的願望.
const html = matchArray.map(crop => {
const regex = RegExp(this.value, 'gi');
const cropName = crop['作物名稱'].replace(regex, `
<span class="hl">${this.value}</span>
`);
const cropYear = crop['年度'].replace(regex, `
<span class="hl">${this.value}</span>
`);
return `
<li>
<span class="name">${cropName}, ${cropYear}</span>
<span class="population">${numberWithCommas(crop['產量'])}</span>
</li>
`;
}).join('');
replace()
也是String
物件的原生方法, 用來替換特定的字串.
有兩個參數, 第一個參數可填入正規表達式, 搜尋所有符合表達式條件的字元組合, 第二個參數填入要替換的內容. 透過這個將和搜尋列相同的字串替換成帶有特定CSS樣式標籤的相同字串, 達到強調的效果.
老樹猴看著自己的資料搜尋列, 喜極而泣. 而他即將撼動整個農產業的陰謀, 就此拉開序幕...
以上就是 JS30 第六篇
練習的程式碼有時可能會有小更動, 和原版不太一樣, JS30是個好課程, 想看原版的教學可以直接前往.
JS30 原版